package com.mathdroid.model.items;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.mathdroid.exceptionhandling.ExceptionHandler;
import com.mathdroid.exceptionhandling.ParserException;
import com.mathdroid.exceptionhandling.XMLErrorHandler;
import com.mathdroid.model.Stats;
import com.mathdroid.model.enums.EArmorProficiency;
import com.mathdroid.model.enums.EClass;
import com.mathdroid.model.enums.EItemSlot;
import com.mathdroid.model.enums.EItemSource;
import com.mathdroid.model.enums.EItemStat;
import com.mathdroid.model.enums.EItemType;
import com.mathdroid.model.enums.EModuleProficiency;
import com.mathdroid.model.enums.ERarity;
import com.mathdroid.model.enums.EWeaponProficiency;

/**
 * This ItemFactory loads all items from the XML files. Also this is a HashMap.
 * Each "type" of items is in another ItemMap. Access those ItemMaps with the 
 * String key of this HashMap.
 * 
 * "Types" are the following different attributes of items:
 * - Each module TYPE has its own ItemMap (Power Crystal, Modulator, ...)
 * - Each weapon PROFICIENCY has its own ItemMap (Assault Rifle, Vibroknive, ..)
 * - Each armor SLOT has its own ItemMap (Head, Relic, ...)
 * 
 * This may be confusing, but is an useful separation of items.
 */
public final class ItemFactory extends HashMap<String, ItemMap> {
	
	/**
	 * Generated UID.
	 */
	private static final long serialVersionUID = -7789697937107373713L;
	
	/**
	 * The document builder to parse the XML files.
	 */
	private DocumentBuilder dBuilder = null;
	
	/**
	 * The "files.xml" document.
	 */
	private Document dFilesXML = null;
	
	/**
	 * The class path to the XML files.
	 */
	private static final String XML_PATH = 
		"/com/mathdroid/model/items/xmldata/";
	
	/**
	 * The file descriptors for each XML item data file.
	 */
	private final LinkedList<File> files = new LinkedList<File>();

	/**
	 * Creates the items for each slot.
	 */
	public ItemFactory() {
		final DocumentBuilderFactory dBuilderFactory = 
			DocumentBuilderFactory.newInstance();
		
		this.buildHashMap();
		
		dBuilderFactory.setValidating(true);
		dBuilderFactory.setNamespaceAware(true);
		
		try {
			this.dBuilder = dBuilderFactory.newDocumentBuilder();
			this.dBuilder.setErrorHandler(new XMLErrorHandler());
			this.parseFilesXML();
			this.parseItems();
			/* NOTE THAT THE SAX PARSER HAS AN OWN REGISTERED ERROR HANDLER */
		} catch (Exception e) {
			ExceptionHandler.report("Could not open or parse XML file.", e);
		}
	}
	
	/**
	 * Initialize all ItemMaps.
	 */
	private void buildHashMap() {
		
		for (EModuleProficiency t 
				: EModuleProficiency.getModuleProficiencies()) {
			this.put(t.toString(), new ItemMap());
		}
		
		for (EItemSlot t : EItemSlot.getArmorSlots()) {
			this.put(t.toString(), new ItemMap());
		}
		
		for (EWeaponProficiency t 
				: EWeaponProficiency.getWeaponProficiencies()) {
			this.put(t.toString(), new ItemMap());
		}
		
		for (EItemSlot s : EItemSlot.getWeaponSlots()) {
			this.put(s.toString(), new ItemMap());
		}
	}

	/**
	 * Parse each item data file. Each file contains item of the same type
	 * and proficiency.
	 * @throws SAXException The file has a invalid syntax.
	 * @throws IOException The file cannot be opened.
	 * @throws ParserException The file contains invalid item data.
	 */
	private void parseItems() 
	throws SAXException, IOException, ParserException {
		Document d = null;
		EItemType t = null;
		
		for (File f : this.files) {
			/*
			 * Note that two of the following three types are always "null" 
			 * for the file.
			 */
			EWeaponProficiency wp = null;
			EArmorProficiency ap = null;
			EModuleProficiency mp = null;
			
			d = this.dBuilder.parse(f);
			t = EItemType.parseEItemType(d.getElementsByTagName("type").item(0)
					.getTextContent());
			
			/*
			 * The single "proficiency" tag in the XML files is used to
			 * define the key for the HashMap. Items are added to the ItemMap
			 * of the key.
			 */
			switch (t) {
				case WEAPON:
					wp = EWeaponProficiency.parseEWeaponProficiency(
							d.getElementsByTagName("proficiency").item(0)
							.getTextContent());
					break;
				case ARMOR:
					ap = EArmorProficiency.parseEArmorProficiency(
							d.getElementsByTagName("proficiency").item(0)
							.getTextContent());
					break;
				case MODULE:
					mp = EModuleProficiency.parseEModuleProficiency(
							d.getElementsByTagName("proficiency").item(0)
							.getTextContent());
					break;
				default:
					break;
			}
			
			/*
			 * Go through all "item" tags in the file.
			 */
			NodeList items = d.getElementsByTagName("item");
			for (int i = 0; i < items.getLength(); i++) {
				Node node = items.item(i);
				Item item = this.buildItem(node);
				item.setType(t);
				item.setArmorProficiency(ap);
				item.setWeaponProficiency(wp);
				item.setModuleProficiency(mp);
				
				
				/*
				 * If the item is a weapon, it has to be added to the 
				 * "item slot" ItemMaps and to the "weapon proficiency" map.
				 */
				if (t == EItemType.WEAPON) {
					for (EItemSlot s : item.getSlots()) {
						this.get(s.toString()).put(item.getName(), item);
					}

					this.get(item.getWeaponProficiency().toString()).put(
							item.getName(), item);
				}
				
				/*
				 * Armor items go into the "item slot" ItemMap and should have 
				 * only one slot.
				 */
				if (t == EItemType.ARMOR) {
					this.get(item.getSlots().get(0).toString()).put(
							item.getName(), item);
				}
				
				/*
				 * Module items go in their "module type" ItemMap.
				 */
				if (t == EItemType.MODULE) {
					if (item.getModuleProficiency() 
							== EModuleProficiency.POWER_CRYSTAL) {
						/*
						 * Add to the general Power Crystal map.
						 */
						this.get(EModuleProficiency.POWER_CRYSTAL.toString())
						.put(item.getName(), item);
						/*
						 * Add to the Power Crystal 1 map.
						 */
						item = (Item) item.clone();
						item.setModuleProficiency(
								EModuleProficiency.POWER_CRYSTAL_1);
						this.get(EModuleProficiency.POWER_CRYSTAL_1.toString())
							.put(item.getName(), item);
						/*
						 * Add to the Power Crystal 2 map.
						 */
						item = (Item) item.clone();
						item.setModuleProficiency(
								EModuleProficiency.POWER_CRYSTAL_2);
						this.get(EModuleProficiency.POWER_CRYSTAL_2.toString())
							.put(item.getName(), item);
					} else {
						this.get(item.getModuleProficiency().toString()).put(
								item.getName(), item);
					}
				}
				
				this.validate(item);
			}
		}
	}

	/**
	 * Do some consistency checks for the item.
	 * @param item The item to validate.
	 */
	private void validate(final Item item) {
		if (item.getName() == null || item.getName().equals("null")) {
			System.err.println("WARNING: The item \"" 
					+ item.getTorHeadID() + "\" has no name.");
		}
		
		if (item.getImageName() == null || item.getImageName().equals("null")) {
			System.err.println("WARNING: The item \"" 
					+ item.getTorHeadID() + "\" has no image name.");
		}
		
		if (item.getRarity() == null) {
			System.err.println("WARNING: The item \"" 
					+ item.getTorHeadID() + "\" has no rarity.");
		}
	}
	
	/**
	 * Parse each part of item data and create an Item object.
	 * @param itemNode The "item" node.
	 * @return Item object.
	 */
	private Item buildItem(final Node itemNode) {
		final Item item = new Item();
		
		NodeList childs = itemNode.getChildNodes();
		Node n = null;
		String nn = null;
		String t = null;
		Stats s = new Stats();
		
		/*
		 * Go through all <item> tags.
		 */
		for (int i = 0; i < childs.getLength(); i++) {
			n = childs.item(i);
			nn = n.getNodeName();
			t = n.getTextContent();
			
			try {
				/*
				 * Single <torheadid> tag.
				 */
				if (nn.equals("torheadid")) {
					item.setTorHeadID(t);
				/*
				 * Single <name> tag.
				 */
				} else if (nn.equals("name")) {
					item.setName(t);
					
				/*
				 * Single <use> tag. (optional)
				 */
				} else if (nn.equals("use")) {
					item.setUseText(t);
				/*
				 * Some <slot> tags. (One or more required)
				 */
				} else if (nn.equals("slot")) {
					boolean valid = false;
					
					for (EItemSlot slot : EItemSlot.getAllSlots()) {
						if (slot.toString().equals(t)) {
							valid = true;
							break;
						}
					}
					
					if (valid) {
						item.addSlot(EItemSlot.parseEItemSlot(t));
					}

				/*
				 * Single <imagename> tag.
				 */
				} else if (nn.equals("imagename")) {
					item.setImageName(t);
				/*
				 * Single <itemlevel> tag.
				 */
				} else if (nn.equals("itemlevel")) {
					item.setItemLevel(Integer.parseInt(t));
				
				/*
				 * Single <rarity> tag.
				 */
				} else if (nn.equals("rarity")) {
					item.setRarity(ERarity.parseERarity(t));
				/*
				 * Single <unique> tag. (optional)
				 * If this tag is missing, the item is considered as not unique.
				 */
				} else if (nn.equals("unique")) {
					item.setUnique(Boolean.parseBoolean(t));

				/* THE FOLLWING CHILDREN HAVE CHILDS, TOO. */
					
				/*
				 * The single <stats> tag.
				 */
				} else if (nn.equals("stats")) {
					this.parseStatTag(n, s);
					item.setStats(s);
				/*
				 * The single <classes> tag. (optional)
				 * If there is no classes tag, every class can obtain the item.
				 */
				} else if (nn.equals("classes")) {
					
					NodeList classes = n.getChildNodes();
					for (int j = 0; j < classes.getLength(); j++) {
						if (classes.item(j).getNodeName().equals("class")) {
							item.getUsableByClass().add(EClass.parseEClass(
									classes.item(j).getTextContent()));	
						}
					}

				/*
				 * The single <sources> tag.
				 */
				} else if (nn.equals("sources")) {
					
					NodeList sources = n.getChildNodes();
					for (int j = 0; j < sources.getLength(); j++) {
						if (sources.item(j).getNodeName().equals("source")) {
							item.getSources().add(EItemSource.parseEItemSource(
									sources.item(j).getTextContent()));
						}
					}

				/*
				 * The single <modules> tag. (optional)
				 * If there is no modules tag, the item has no module slots.
				 */
				} else if (nn.equals("modules")) {
					
					NodeList modules = n.getChildNodes();
					for (int j = 0; j < modules.getLength(); j++) {
						if (modules.item(j).getNodeName().equals("module")) {
							NodeList moduleChildren =
								modules.item(j).getChildNodes();
							boolean hasStats = false;
							EModuleProficiency mp = null;
							String moduleTextContent = null;
							int moduleItemLevel = 0;
							Stats moduleStats = null;
							
							/*
							 * The <module> tag contains: 
							 * - <type> (required)
							 * - <itemlevel> (optional)
							 * - <stats> (optional)
							 */
							for (int k = 0; 
									k < moduleChildren.getLength(); 
									k++) {
								
								moduleTextContent = 
									moduleChildren.item(k).getTextContent();
								
								if (moduleChildren.item(k).getNodeName()
										.equals("type")) {
									mp = EModuleProficiency
									.parseEModuleProficiency(moduleTextContent);
								} else if (moduleChildren.item(k).getNodeName()
										.equals("itemlevel")) {
									moduleItemLevel = Integer
									.parseInt(moduleTextContent);
								} else if (moduleChildren.item(k).getNodeName()
										.equals("stats")) {
									moduleStats = new Stats();
									this.parseStatTag(
										moduleChildren.item(k), moduleStats);
									hasStats = true;
								}
							}
							
							/*
							 * If the module has stats, compile a module
							 * item and add it to its parent item.
							 */
							if (hasStats) {
								Item moduleItem = new Item();
								moduleItem.setModuleProficiency(mp);
								moduleItem.setItemLevel(moduleItemLevel);
								moduleItem.setStats(moduleStats);
								item.addDefaultModule(moduleItem);
							} else {
								/*
								 * if the module has no stats add an empty
								 * module slot to the parent item.
								 */
								item.getModules().put(mp, null);
							}
						}
					}

				}
			} catch (Exception e) {
				ExceptionHandler.report("Could not parse item data.", e);
			}
		}
		
		return item;
	}

	/**
	 * Parse a "stat" tag and add the stats to the Stats object.
	 * @param n The "stats" node.
	 * @param s The Stats object to fill with stats.
	 */
	private void parseStatTag(final Node n, final Stats s) {
		String nn = null;
		String t = null;
		NodeList statsNode = n.getChildNodes();
		for (int j = 0; j < statsNode.getLength(); j++) {
			nn = statsNode.item(j).getNodeName();
			t = statsNode.item(j).getTextContent();

			/* 
			 * Go through all possible item stats and check if the 
			 * name matches. For example <aim> or <strength>.
			 */
			for (EItemStat estat : EItemStat.getAllStats()) {
				if (nn.equals(estat.getXMLtag())) {
					if (estat != EItemStat.MAX_DAMAGE 
							&& estat != EItemStat.MIN_DAMAGE) {
						s.put(estat, Integer.parseInt(t));
					} else {
						s.put(estat, (int) Double.parseDouble(t));
					}
					
				}
			}
		}
	}

	/**
	 * Open an item data file.
	 * @param xmlFile Name of the file, do not add ".xml" suffix.
	 * @return File object.
	 * @throws URISyntaxException Invalid URI syntax.
	 */
	private File openFile(final String xmlFile) throws URISyntaxException {
		final URL filesURL = 
			this.getClass().getResource(XML_PATH + xmlFile + ".xml");
		File f = new File(filesURL.toURI());

		return f;
	}

	/**
	 * Open "files.xml" and open all of the listed item data files.
	 * @throws URISyntaxException Invalid URI syntax.
	 * @throws SAXException "files.xml" has an invalid syntax.
	 * @throws IOException "files.xml" cannot be opened.
	 */
	private void parseFilesXML() 
	throws URISyntaxException, SAXException, IOException {

		File filesXML = this.openFile("files");
		this.dFilesXML = this.dBuilder.parse(filesXML);
		this.dFilesXML.getDocumentElement().normalize();
		
		NodeList fileNames = 
			this.dFilesXML.getDocumentElement().getChildNodes();
		
		String name = null;
		for (int i = 0; i < fileNames.getLength(); i++) {
			name = fileNames.item(i).getTextContent();
			if (this.isElement(name)) {
				this.files.add(this.openFile(name));
			}
		}
	}
	
	/**
	 * A node is an valid element if it contains more than Whitespaces.
	 * @param s Text.
	 * @return If the text is a valid element.
	 */
	private boolean isElement(final String s) {
		boolean result = true;
		
		if (s.trim().equals("")) {
			result = false;
		}
		
		return result;
	}
}
